import { describe, it, expect, vi } from 'vitest';

import { isFailure, isResult, isSuccess, makeResult, unwrap } from '../Result';

describe('Result', () => {
  describe('makeResult', () => {
    describe('Ok', () => {
      it('should return a Success result', () => {
        const result = makeResult.Ok(1);
        expect(result.type).toBe('ok');
        expect(result.value).toBe(1);
      });

      it('should have correct methods', () => {
        const result = makeResult.Ok(1);
        expect(typeof result.map).toBe('function');
        expect(typeof result.chain).toBe('function');
        expect(typeof result.mapError).toBe('function');
        expect(typeof result.match).toBe('function');
      });
    });

    describe('Err', () => {
      it('should return a Failure result', () => {
        const result = makeResult.Err('error');
        expect(result.type).toBe('err');
        expect(result.error).toBe('error');
      });

      it('should have correct methods', () => {
        const result = makeResult.Err('error');
        expect(typeof result.map).toBe('function');
        expect(typeof result.chain).toBe('function');
        expect(typeof result.mapError).toBe('function');
        expect(typeof result.match).toBe('function');
      });
    });
  });

  describe('isResult', () => {
    it('should return true for Success result', () => {
      expect(isResult(makeResult.Ok(1))).toBe(true);
    });

    it('should return true for Failure result', () => {
      expect(isResult(makeResult.Err('error'))).toBe(true);
    });

    it('should return false for non-result objects', () => {
      expect(isResult({})).toBe(false);
      expect(isResult(null)).toBe(false);
      expect(isResult(undefined)).toBe(false);
      expect(isResult(1)).toBe(false);
      expect(isResult('string')).toBe(false);
      expect(isResult({ type: 'ok' })).toBe(false); // Missing value
      expect(isResult({ type: 'err' })).toBe(false); // Missing error
      expect(isResult({ value: 1 })).toBe(false); // Missing type
    });
  });

  describe('isSuccess', () => {
    it('should return true for Success result', () => {
      expect(isSuccess(makeResult.Ok(1))).toBe(true);
    });

    it('should return false for Failure result', () => {
      expect(isSuccess(makeResult.Err('error'))).toBe(false);
    });
  });

  describe('isFailure', () => {
    it('should return true for Failure result', () => {
      expect(isFailure(makeResult.Err('error'))).toBe(true);
    });

    it('should return false for Success result', () => {
      expect(isFailure(makeResult.Ok(1))).toBe(false);
    });
  });

  describe('unwrap', () => {
    it('should return value if result is Success', () => {
      const result = makeResult.Ok(100);
      expect(unwrap(result)).toBe(100);
      expect(result.unwrap()).toBe(100);
    });

    it('should throw error if result is Failure', () => {
      const error = new Error('fail');
      const result = makeResult.Err(error);
      expect(() => unwrap(result)).toThrow(error);
      expect(() => result.unwrap()).toThrow(error);
    });

    it('should throw Error object if failure contains non-Error', () => {
      const result = makeResult.Err('string error');
      expect(() => unwrap(result)).toThrow('string error');
      expect(() => result.unwrap()).toThrow('string error');
      try {
        result.unwrap();
      } catch (e) {
        expect(e).toBeInstanceOf(Error);
        expect((e as Error).message).toBe('string error');
      }
    });
  });

  describe('unwrapOr', () => {
    it('should return value if result is Success', () => {
      const result = makeResult.Ok(100);
      expect(result.unwrapOr(200)).toBe(100);
    });

    it('should return default value if result is Failure', () => {
      const result = makeResult.Err<number>('error');
      expect(result.unwrapOr(200)).toBe(200);
    });
  });

  describe('Success methods', () => {
    describe('map', () => {
      it('should transform the value', () => {
        const result = makeResult.Ok(1).map(x => x + 1);
        expect(unwrap(result)).toBe(2);
      });
    });

    describe('chain', () => {
      it('should chain with another Result', () => {
        const result = makeResult.Ok(1).chain(x => makeResult.Ok(x + 1));
        expect(unwrap(result)).toBe(2);
      });

      it('should chain with a Failure', () => {
        const result = makeResult.Ok(1).chain(() => makeResult.Err('fail'));
        expect(isFailure(result)).toBe(true);
        expect((result as any).error).toBe('fail');
      });
    });

    describe('mapError', () => {
      it('should not transform the error (no-op for Success)', () => {
        const result = makeResult.Ok(1).mapError(() => 'new error');
        expect(unwrap(result)).toBe(1);
      });
    });

    describe('match', () => {
      it('should call ok handler', () => {
        const result = makeResult.Ok(1);
        const output = result.match({
          ok: val => `ok: ${val}`,
          err: err => `err: ${err}`,
        });
        expect(output).toBe('ok: 1');
      });
    });
  });

  describe('Failure methods', () => {
    describe('map', () => {
      it('should not transform the value (no-op for Failure)', () => {
        const result = makeResult.Err('error').map((x: any) => x + 1);
        expect(isFailure(result)).toBe(true);
        expect((result as any).error).toBe('error');
      });
    });

    describe('chain', () => {
      it('should not call the function (no-op for Failure)', () => {
        const fn = vi.fn();
        const result = makeResult.Err('error').chain(fn);
        expect(fn).not.toHaveBeenCalled();
        expect(isFailure(result)).toBe(true);
      });
    });

    describe('mapError', () => {
      it('should transform the error', () => {
        const result = makeResult.Err('error').mapError(e => e.toUpperCase());
        expect(isFailure(result)).toBe(true);
        expect((result as any).error).toBe('ERROR');
      });
    });

    describe('match', () => {
      it('should call err handler', () => {
        const result = makeResult.Err('error');
        const output = result.match({
          ok: val => `ok: ${val}`,
          err: err => `err: ${err}`,
        });
        expect(output).toBe('err: error');
      });
    });
  });

  describe('Edge Cases: Nullish values', () => {
    it('should handle null as a valid Success value', () => {
      const result = makeResult.Ok(null);
      expect(isSuccess(result)).toBe(true);
      expect(unwrap(result)).toBeNull();
    });

    it('should handle undefined as a valid Success value', () => {
      const result = makeResult.Ok(undefined);
      expect(isSuccess(result)).toBe(true);
      expect(unwrap(result)).toBeUndefined();
    });

    it('should allow mapping over null values', () => {
      const result = makeResult
        .Ok(null)
        .map(val => (val === null ? 'was null' : 'not null'));
      expect(unwrap(result)).toBe('was null');
    });
  });

  describe('Control Flow: Exceptions', () => {
    it('should NOT catch errors thrown inside map (Sync/Pure behavior)', () => {
      const result = makeResult.Ok(1);

      // If your architecture intends to catch this, change .toThrow() to checking for Err
      expect(() => {
        result.map(() => {
          throw new Error('Boom');
        });
      }).toThrow('Boom');
    });

    it('should NOT catch errors thrown inside chain', () => {
      const result = makeResult.Ok(1);

      expect(() => {
        result.chain(() => {
          throw new Error('Boom');
        });
      }).toThrow('Boom');
    });
  });

  describe('isResult Edge Cases', () => {
    it('should return false if type discriminator is invalid', () => {
      // Has correct shape keys, but wrong 'type' value
      const fakeResult = { type: 'banana', value: 1 };
      expect(isResult(fakeResult)).toBe(false);
    });

    it('should return false for extra properties mimicking result', () => {
      // Depends on how strict your guard is.
      // Usually, duck typing accepts this, but good to know.
      const complicatedObj = { type: 'ok', value: 1, extra: 'stuff' };
      expect(isResult(complicatedObj)).toBe(true); // Should likely be true
    });
  });

  describe('Integration: Pipelining', () => {
    it('should handle a sequence of maps and chains', () => {
      const result = makeResult
        .Ok(10)
        .map(x => x * 2) // 20
        .chain(x => makeResult.Ok(x + 5)) // 25
        .mapError(() => 'new error') // Ignored
        .map(x => `Value: ${x}`); // "Value: 25"

      expect(unwrap(result)).toBe('Value: 25');
    });

    it('should short-circuit the pipeline on first error', () => {
      const fn = vi.fn();

      const result = makeResult
        .Ok(10)
        .map(x => x * 2) // 20
        .chain(() => makeResult.Err('First Failure')) // Breaks here
        .map(fn) // Should skip
        .chain(x => makeResult.Ok(x)); // Should skip

      expect(fn).not.toHaveBeenCalled();
      expect(isFailure(result)).toBe(true);
      expect((result as any).error).toBe('First Failure');
    });
  });
});
